Queues
Introduction
Queue is a simple but very powerful data structure to solve numerous computer applications. Like stacks, queues are also useful to solve various system programs. Let us visit some simple applications of queues in our everyday life as well as in computer science before going to study this data structure.
Queuing in front of a counter
Suppose there are a number of customer in front of a counter to get service (say, to collect tickets or to withdraw/deposit money in a teller of a bank), Figure 7.1(a). The customers are forming a queue and they will be served in the order they arrived, that is, a customer who comes first will be served first.
Fig. 7.1(a) Queue of
customers.
Traffic control at a turning point
Suppose there is a turning point in a highway where the traffics has to turn, Figure 7.1(b). All the traffics will wait on a line till it gets the signal for moving. And on getting the ‘Go’ signal the vehicles will turn on a first come first turn basis.
Fig. 7.1(b) Traffic
passing at a turning point.
Process synchronization in multi-user environment
In multi-user environment, more than one processes are to be handled by the monitor (operating system), Figure 7.1(c). The three different states that a process may have: READY, RUNNING and AWAITED. Processes are in READY state when they are submitted to the system for execution. A process is in RUNNING state if it is currently under execution. Similarly a process will be transferred to the AWAITED state when it requires resource(s) which is/are busy. In order to synchronize the execution of processes, monitor has to maintain two queues namely Q1 and Q2 for READY and AWAITED states respectively where processes entered a queue first will be exited first.
Fig. 7.1(c) Queues of processes.
Resource sharing in a computer centre
In a computer centre, where resources are limited as compared to the demand, user must sign a waiting register, Figure 7.1(d). The user, who has been waiting for a terminal for the longest period of time gets hold of the resource first then the second candidate and so on. Here the waiting list maintains a queue and the first signed will be allowed first.
Fig. 7.1(d) Waiting
queue of users in computer centre.
Definition
Like stack, a queue is an ordered collection of
homogeneous data elements; in contrast with the stack, here, insertion and
deletion operations take place at two extreme ends.
Queue
is also a linear data structure like array, stack and linked list where
ordering of the elements are in linear fashion. The only difference between
stack and queue is that in case of stack insertion and deletion (PUSH and POP)
operations are at one end (TOP) only, but in a queue insertion (called ENQUEUE)
and deletion (called DEQUEUE) operations take place at two ends called REAR and
FRONT of the queue respectively. Figure 7.2 represents a model of queue
structure.
Fig. 7.2 Model of a queue.
Elements in a queue are termed as ITEM; the number of elements that a queue can accommodate is termed as LENGTH.
From the examples as mentioned and the definition stated above, it is evident that data in queue is processed in the same order as it was entered, that is, on a first in, first out basis. This is why, queue is also alternatively termed as first-in first-out (FIFO).
Representation
of Queues
There are two ways to represent a queue in memory:
1.
Using an array
2.
Using linked list
First kind of representation uses one-dimensional array and it is a better choice where a queue of fixed size is required. The other representation uses a double linked list and provides a queue whose size can vary during processing.
Following two sub sections are to describe the representation of queues in memory.
Representation
of Queue using Array
A one-dimensional array, say Q[1 . . . N] can be used to represent a queue. Figure 5.3 shows an instance of such a queue. With this representation, two pointers, namely, FRONT and REAR are used to indicate two ends of the queue. For the insertion of the next element, pointer REAR will be consultant and for deletion pointer FRONT will be consultant.
Fig. 5.3 Array
representation of a queue.
Three states of the queue with this representation are as below:
Queue is empty
FRONT = 0
REAR
= 0 (and/or)
Queue
is full
REAR
= N
FRONT =
1 (when full by compact)
Queue
contains element ³ 1
FRONT
£
REAR
Number of element = REAR – FRONT
+ 1
Now let us define the operation ENQUEUE to insert an
element into a queue.
Algorithm Enqueue
Input: An element ITEM
that has to be inserted.
Output: The ITEM is at the
REAR of the queue.
Data
structure: Q
is the array representation of queue structure; two pointers FRONT and REAR of
the
queue Q are known.
Steps:
1. |
If (REAR = N) then
// Queue is full |
||
2. |
|
Print “Queue is full” |
|
3. |
|
Exit |
|
4. |
Else |
||
5. |
|
If (REAR = 0) and (FRONT =
0) then // Queue is empty |
|
6. |
|
|
FRONT
= 1 |
7. |
|
EndIf |
|
8. |
|
REAR
= REAR + 1 // Insert the item into the queue at
REAR |
|
9. |
|
Q[REAR] = ITEM |
|
10. |
EndIf |
||
11. |
Stop |
The deletion operation DEQUEUE can be defined as below:
Algorithm Dequeue
Input: A queue with elements. FRONT and REAR are the two
pointers of the queue Q.
Output: The deleted
element is stored in ITEM.
Data
structures: Q is the array representation
of queue structure.
Steps:
1. |
If (FRONT = 0) then |
||
2. |
|
Print “Queue is empty” |
|
3. |
|
Exit |
|
4. |
Else |
||
5. |
|
ITEM
= Q[FRONT] // Get
the element |
|
6. |
|
If (FRONT = REAR) // When queue contains single element |
|
7. |
|
|
REAR
= 0 // The queue becomes
empty |
8. |
|
|
FRONT
= 0 |
9. |
|
Else |
|
10. |
|
|
FRONT
= FRONT + 1 |
11. |
|
EndIf |
|
12. |
EndIf |
||
13. |
Stop |
Let us trace the above two algorithms with a queue of size = 10. Suppose, the current state of the queue is FRONT = 8, REAR = 9. Ten operations are requested as under:
1. DEQUEUE 2. ENQUEUE 3. ENQUEUE
4.
DEQUEUE 5. DEQUEUE 6. DEQUEUE
7.
ENQUEUE 8. ENQUEUE 9. DEQUEUE
10. DEQUEUE
Figure 7.4 presents the status of the queue when these operations are carried out.
Fig. 7.4 Operations
on a queue.
There is one potential problem with this representation. From Figure 7.4, we can see that with this representation, a queue may not be full, still a request for insertion operation may be denied. For an example, on request (3) (in Figure 7.4) 8 rooms are available but insertion is not possible as insertion pointer reaches the end of the queue. This is simply a wastage of the storage. This type of representation is recommendable for such application where the queue is emptied at certain interval.
Representation
of Queue using Linked List
One more limitation of a queue, other than the
inadequate service of insertion represented with array, is the rigidness of its
length. In several applications, length of the queue may not be predicated
before and it varies abruptly. To overcome this problem, another preferable
representation of queue is with linked list. Here, we select double linked list
which allows us to move both the ways. Figure 7.5 shows the double, linked list
representation of a queue. Pointers FRONT and REAR point the first node and the
last node in the list.
Fig. 7.5 A double linked list representation
of queue.
Two
states of the queue, namely, empty or it contains some element can be judged by
the following tests:
Queue
is empty
FRONT
= REAR = HEADER
HEADER→RLINK
= NULL
Queue
contains at least one element
HEADER→RLINK ¹ NULL
The insertion and deletion operations are
straightforward and the same as in the algorithm InsertEnd_DL (for Enqueue)
and algorithm DeleteFront_DL (for Dequeue); these two algorithms are
already defined in Module 5 (Linked List).
Various
Queue Structures
So far we have discussed two different queue
structures, that is, using array and linked list (and a variation of queue
structure using array as an assignment). Other than these, there are more known
queue structures. This section discusses about them.
Circular
Queue
For a queue represented using array when the REAR
pointer reaches at the end, insertion will be denied even if room is available
at the front. One way to avoid this is using a circular array. Physically, a
circular array is the same as ordinary array, say, A[1 . . . N], but logically it implies that A[1] comes after A[N] or after A[N],
A[1] appears. Figure 7.6 shows
logical and physical representations for a circular array.
Fig. 7.6 Logical and physical views of a
circular queue.
The
principle for representation of a circulation array is as stated below:
Both
pointers will move in clockwise direction. This is controlled by the mod operation; for example, if the
current pointer is at i then shift to the next location will be i
mod LENGTH + 1, 1 £
i £ LENGTH (where LENGTH is the
queue length). Thus, if i = LENGTH (that is at the end), then next
position for the pointer is 1.
With
this principle the two states of the queue regarding its empty or full will be
decided as:
Circular
queue is empty
FRONT
= 0
REAR
= 0
Circular
queue is full
FRONT = (REAR mod LENGTH) + 1
Following
two algorithms describe the insertion and deletion operations on a circular
queue.
Algorithm Enqueue_CQ
Input: An element ITEM
to be inserted into the circular queue.
Output: Circular queue
with the ITEM at FRONT, if the queue is not full.
Data
structures: CQ
be the array to represent the circular queue. Two pointers FRONT and REAR are
known.
Steps:
1. |
If (FRONT = 0) then // When queue is empty |
||
2. |
|
FRONT
= 1 |
|
3. |
|
REAR
= 1 |
|
4. |
|
CQ[FRONT]
= ITEM |
|
5. |
Else
// Queue is not empty |
||
6. |
|
next
= (REAR mod LENGTH) + 1 |
|
7. |
|
If (next ¹ FRONT) then // If queue
is not full |
|
8. |
|
|
REAR
= next |
9. |
|
|
CQ[REAR]
= ITEM |
10. |
|
Else |
|
11. |
|
|
Print “Queue is full” |
12. |
|
EndIf |
|
13. |
EndIf |
||
14. |
Stop |
Algorithm Dequeue_CQ
Input: A queue CQ with
elements. Two pointers FRONT and REAR are known.
Output: The deleted
element is ITEM if the queue is not empty.
Data
structures: CQ
is the array representation of circular queue.
Steps:
1. |
If (FRONT = 0) then |
||
2. |
|
Print “Queue is empty” |
|
3. |
|
Exit |
|
4. |
Else |
||
5. |
|
ITEM
= CQ[FRONT] |
|
6. |
|
If (FRONT = REAR) then // If the queue
contains single element |
|
7. |
|
|
FRONT
= 0 |
8. |
|
|
REAR
= 0 |
9. |
|
Else |
|
10. |
|
|
FRONT
= (FRONT mod LENGTH) + 1 |
11. |
|
EndIf |
|
12. |
EndIf |
||
13. |
Stop |
In
order to trace these two algorithms let us consider a circular queue of LENGTH
= 4. Following operations are requested. Different states of the queue under
processing these requests is illustrated in Figure 7.7.
1. ENCQUEUE (A) 2. ENCQUEUE (B)
3. ENCQUEUE (C) 4. ENCQUEUE (D)
5. DECQUEUE 6. ENCQUEUE
(E)
7. DECQUEUE
8. ENCQUEUE (F)
9. DECQUEUE 10. DECQUEUE
11. DECQUEUE 12. DECQUEUE
Assume
that initially queue is empty, that is, FRONT = REAR = 0.
Fig. 7.7 Trace of insertion and deletion
operations on a circular queue.
Deque
Another variation of the queue is known as deque
(may be pronounced as ‘deck’). Unlike queue, in deque, both insertion and
deletion operations can be made at either end of the structure. Actually, the
term deque is originated from double
ended queue. Such a structure is shown in Figure 7.8.
Fig. 7.8 A deque structure.
It is
clear from the deque structure that it is a general representation of both
stack and queue. In other words, a deque can be used as a stack as well as a
queue.
There
are various ways of representing a deque in a computer. One simpler way to
represent it is using a double linked list. Another popular representation is
using a circular array (as used in circular queue).
Following four operations are possible on a deque
which consists of a list of items:
1.
Push_DQ(ITEM) : To insert ITEM at the FRONT end of deque
2. Pop_DQ(
): To remove the FRONT item from deque
3.
Inject(ITEM): To insert ITEM at the REAR end of deque.
4. Eject(
): To remove the REAR ITEM from deque.
These
operations are described for a deque based on a circular array of length
LENGTH. Let the array be DQ[1 . . . LENGTH].
Algorithm Push_DQ
Input: ITEM to be
inserted at the FRONT.
Output: Deque with newly
inserted element ITEM if it is not full already.
Data
structures: DQ
being the circular array representation of deque.
Steps:
1. |
If (FRONT = 1) then // If FRONT is at
extreme left |
||
2. |
|
ahead = LENGTH |
|
3. |
Else
// If FRONT is at extreme right or deque is
empty |
||
4. |
|
If (FRONT = LENGTH) or
(FRONT = 0) then |
|
5. |
|
|
ahead = 1 |
6. |
|
Else |
|
7. |
|
|
ahead = FRONT – 1 // FRONT is at an
intermediate position |
8. |
|
EndIf |
|
9. |
|
If (ahead = REAR) then |
|
10. |
|
|
Print “Deque is full” |
11. |
|
|
Exit |
12. |
|
Else |
|
13. |
|
|
FRONT = ahead //
Push the ITEM |
14. |
|
|
DQ[FRONT] = ITEM |
15. |
|
EndIf |
|
16. |
EndIf |
||
17. |
Stop |
Algorithm Pop_DQ
/* This
algorithm is same as the algorithm Dequeue_CQ
*/
Algorithm Inject
/* This algorithm is same as
the algorithm Enqueue_CQ */
Algorithm Eject_DQ
Input: A deque with
elements in it.
Output: The item is
deleted from the REAR end.
Data
structures: DQ
being the circular array representation of deque.
Steps:
1. |
If (FRONT = 0) then |
||||
2. |
|
Print “Deque is empty” |
|||
3. |
|
Exit |
|||
4. |
Else |
||||
5. |
|
If (FRONT = REAR) then // The deque contains single element |
|||
6. |
|
|
ITEM
= DQ[REAR] |
||
7. |
|
|
FRONT
= REAR = 0 // Deque becomes empty |
||
8. |
|
Else |
|||
9. |
|
|
If (REAR = 1) then // REAR is at extreme
left |
||
10. |
|
|
|
ITEM
= DQ[REAR] |
|
11. |
|
|
|
REAR
= LENGTH |
|
12. |
|
|
Else |
||
13. |
|
|
|
If (REAR = LENGTH) then // REAR is at extreme right |
|
14. |
|
|
|
|
ITEM
= DQ[REAR] |
15. |
|
|
|
|
REAR
= 1 |
16. |
|
|
|
Else // REAR is at an intermediate
position |
|
17. |
|
|
|
|
ITEM
= DQ[REAR] |
18. |
|
|
|
|
REAR
= REAR – 1 |
19. |
|
|
|
EndIf |
|
20. |
|
|
EndIf |
||
21. |
|
EndIf |
|||
22. |
EndIf |
||||
23. |
Stop |
There
are, however, two variations of deque known:
1.
Input restricted deque, and
2.
Output restricted deque.
These
two types are actually intermediate between a queue and a deque. Specifically,
an input-restricted deque is a deque which allows insertions at one end
(say REAR end) only but allows deletions at both ends. Similarly, an output-restricted
deques is a deque where deletions take place at one end (say FRONT end)
only but allows insertions at both ends. Figure 7.9 represents two such
variations of deques.
Fig. 7.9 Types of deque.
Priority
Queue
A priority queue is another variation of
queue structure. Here, each element has been assigned a value, called
priority of the element, and an element can be inserted or deleted not only
at the ends but at any position on the queue. Figure 7.10 shows a priority
queue.
Fig. 7.10 View of a priority queue.
With
this structure, an element X of priority pi may be deleted before an
element which is at FRONT. Similarly, insertion of an element is based on its
priority, that is, instead of adding it after the REAR it may be inserted at an
intermediate position dictated by its priority value.
Note
that, the name priority queue is a misnomer in the sense that the data
structure is not a queue as per the definition; priority queue does not
strictly follow first-in first-out (FIFO) principle which is the basic
principle of a queue. Nevertheless, the name is now firmly associated with this
particular data type. However, there are various models of priority queue known
in different applications. Let us consider a particular model of priority
queue.
1. An
element of higher priority is processed before any element of lower priority.
2. Two elements with the same priority are
processed according to the order in which they were added to the queue.
Here, process means two basic operations namely
insertion or deletion. There are various ways of implementing the structure of
a priority queue. These are
(i) Using a simple/circular array
(ii) Multi-queue implementation
(iii) Using a double linked list, and
(iv) Using
heap tree.
Priority queue using an array
With this representation, an array can be maintained
to hold the item and its priority value. The element will be inserted at the
REAR end as usual. The deletion operation will then be performed either of the
two following ways:
(a) Starting
from the FRONT pointer, traverse the array for an element of the highest
priority. Delete this element from the queue. If this is not the front most
element shift all its trailing elements after the deleted element one stroke
each to fill up the vacant position (see Figure 7.11).
Fig.
7.11 Deletion
operation in an array representation of a priority queue.
This
implementation, however, is very inefficient as it involves searching the queue
for the highest priority element and shifting the trailing elements after the
deletion. A better implementation is as follows:
(b) Add
the elements at the REAR end as earlier. Using a stable sorting algorithm*, sort the elements of the
queue so that the highest priority elements is at the FRONT end. When a
deletion is required, delete it from the FRONT end only (see Figure 7.12).
Fig.
7.12 Another
array implementation of a priority queue.
The
second implementation is comparatively better than the first one, here the only
burden is to sort the elements. The algorithms of the above two implementations
are left as assignments to the reader.
Multi-queue implementation
This implementation assumes N different
priority values. For each priority Pi there are two pointers Fi and Ri corresponding to FRONT and
REAR pointer respectively. The elements between Fi and Ri are all of equal priority
value Pi. Figure 5.14 represents a
view of such structure.
Fig. 5.14 Multi-queue representation of
a priority queue.
With
this representation, an element with priority value Pi will consult Fi for deletion and Ri for insertion. But this
implementation is associated with number of difficulties:
(i) It
may lead to a huge shifting in order to make a room for an item to be inserted.
(ii) Large number of pointers are involved when
the range of priority values are large.
In addition to the above, there are two other
techniques to represent multi-queue, which are shown in Figures 7.14(a) and 7.14(b).
It is
clear from Figure 7.14(a) that for each priority value a simple queue is to be
maintained. An element will be added into a particular queue depending on its
priority value.
The
priority queue as shown in Figure 7.14(b) is some way better than the
multi-queue with multiple queues. Here one can get rid off maintaining several
pointers for FRONT and REAR in several queues. Multi-queue with multiple queues
has one advantage that one can have different queues of arbitrary length. In
some application, it is seen that, number of occurrence of elements with some
priority value is much larger than other value, thus demanding a queue of
larger size.
Fig. 7.14 Multi-queue implementation
with multiple simple queues and matrix.
Both
the above representations are not economic from the memory utilization point of
view; majority of the memory space remains vacant.
Algorithms
for insertion and deletion operations for multi-queue implementation are left
as exercises.
Linked list representation of priority queue
This representation assumes the node structure as
shown in Figure 7.15. LLINK and RLINK are two usual link fields, DATA is to
store the actual content and PRIORITY is to store the priority value of the
item. We will consider FRONT and REAR as two pointers pointing the first and
last node in the queue, respectively. Here all the nodes are in sorted order
according to the priority values of the items in the nodes. Following is an
instance of priority queue:
Fig. 7.15 Linked list representation of
a priority queue.
With
this structure, to delete an item having priority p, the list will be
searched starting from the node under pointer REAR and the first occurring node
with PRIORITY = p will be deleted. Similarly, to insert a node
containing an item with priority p, the search will begin from node under
pointer FRONT and node will be inserted before a node found first with priority
value p, or if not found then before the node with the next priority
value. The following two algorithms Insert_PQ
and Delete_PQ are used to implement
the insertion and deletion operations on a priority queue.
Algorithm Insert_PQ
Input: The ITEM and its
priority P value of a node that is to be inserted.
Output: A new node
inserted.
Data
structures: Linked
list structure of priority queue; HEADER as the pointer to the header.
Steps:
1. |
ptr
= HEADER
// Start
from the first node |
||
2. |
new
= GetNode(NODE) //
Avail a new node |
||
3. |
new→DATA
= ITEM
// Get initialized the
node with ITEM |
||
4. |
new→PRIORITY
= P |
||
5. |
While (ptr→RLINK ¹ NULL) and (ptr→PRIORITY
< P) do // Search for the position |
||
6. |
|
ptr
= ptr→RLINK |
|
7. |
EndWhile |
||
8. |
If (ptr→RLINK = NULL) then // If the list is empty or the item is with
the largest priority value
|
||
9. |
|
ptr→RLINK
= new |
|
10. |
|
new→LLINK
= ptr |
|
11. |
|
new→RLINK
= NULL // The node is inserted as the last
node |
|
12. |
|
REAR
= new |
|
13. |
Else |
||
14. |
|
If (ptr→PRIORITY ³ P) then // First occurrence
is found |
|
15. |
|
|
ptr1
= ptr→.LLINK // Insert
the new node |
16. |
|
|
ptr1→RLINK
= new // Before the node with priority
> P |
17. |
|
|
new→RLINK
= ptr |
18. |
|
|
ptr→LLINK
= new |
19. |
|
|
new→LLINK
= ptr1 |
20. |
|
EndIf |
|
21. |
EndIf |
||
22. |
FRONT
= HEADER→RLINK // Set the FRONT
pointer |
||
23. |
Stop |
Similarly,
the algorithm for deletion can be described as below:
Algorithm Delete_PQ
Input: The priority P
of the element which has to be deleted.
Output: The element that is being
deleted.
Data
structures: Linked
list structure of priority queue; HEADER as the pointer to the header.
Steps:
1. |
If (REAR = NULL) then |
||||
2. |
|
Print “Queue is empty” |
|||
3. |
|
Exit |
|||
4. |
Else |
||||
5. |
|
ptr
= REAR |
|||
6. |
|
While (ptr→PRIORITY > P)
and (ptr ¹ HEADER) do |
|||
7. |
|
|
ptr
= ptr→LLINK |
||
8. |
|
EndWhile |
|||
9. |
|
If (ptr = HEADER) or (ptr→PRIORITY
< P) |
|||
10. |
|
|
Print “No item with priority”, P |
||
11. |
|
|
Exit |
||
12. |
|
Else |
|||
13. |
|
|
If (ptr→priority = P)
then |
||
14. |
|
|
|
ptr1
= ptr→LLINK |
|
15. |
|
|
|
ptr2
= ptr→RLINK |
|
16. |
|
|
|
If (ptr = REAR) // If the last node to be deleted |
|
17. |
|
|
|
|
REAR
= ptr1 |
18. |
|
|
|
|
ptr1→RLINK
= NULL |
19. |
|
|
|
Else // Other than last node |
|
20. |
|
|
|
|
ptr1→RLINK
= ptr2 //
Deleted |
21. |
|
|
|
|
ptr2→LLINK
= ptr1 |
22. |
|
|
|
EndIf |
|
23. |
|
|
EndIf |
||
24. |
|
EndIf |
|||
25. |
|
item
= ptr→DATA |
|||
26. |
|
ReturnNode(item) |
|||
27. |
EndIf |
||||
28. |
Stop |
Application of Queues
Numerous applications of queue structures are known
in computer science. One major application of queue is in simulation. Another
important application of queue is observed in implementation of various aspects
of operating system. Multiprogramming environment uses several queues to
control various programs. And, of course, queues are very much useful to
implement various algorithms. For example, various scheduling algorithms are
known to use varieties of queue structures.
This
section highlights few applications and then illustrates how powerful queues
are there to solve different problems.
CPU Scheduling in Multiprogramming
Environment
In a multiprogramming environment, a single CPU has
to serve more than one program simultaneously. This section gives a brief idea
about how queues are important to manage various programs in such an
environment.
Let us
consider a multiprogramming environment where possible jobs to the CPU are
categorized into three groups:
1. Interrupts
to be serviced. Variety of devices and terminals are connected with the CPU and
they may interrupt at any moment to get a service from it.
2. Interactive
users to be serviced. These are mainly student’s programs in various terminals
under execution.
3. Batch jobs to be serviced.
These
are long term jobs mainly from the non-interactive users, where all the inputs
are fed when jobs are submitted; simulation programs, and jobs to print
documents are of this kind.
Here
the problem is to schedule all sorts of jobs so that the required level of
performance of the environment will be attained. One way to implement complex
scheduling is to classify the workload according to its characteristics and to
maintain separate process queues. So far the environment is concerned, we can
maintain three queues, as depicted in Figure 7.19. This approach is often
called multi-level queues scheduling. Process will be assigned to their
respective queues. CPU will then service the processes as per the priority of
the queues. In case of a simple strategy, absolute priority, the process from
the highest priority queue (for example, system processes) are serviced until
the queue becomes empty. Then CPU switches to the queue of interactive
processes which is having medium-priority and so on. A lower-priority process
may, of course, be pre-empted by a higher-priority arrival in one of the
upper-level queues.
Fig. 7.19 Process scheduling with
multi-level queues.
Multi-level
queues strategy is a general discipline but has some drawbacks. The main
drawback is that when process arrived in higher-priority queues is very high,
the processes in lower-priority queue may starve for a long time. One way out
to solve this problem is to time slice between the queues. Each queue gets a
certain portion of the CPU time. Another possibility is known as multi-level
feedback queue strategy. Normally in multi-level queue strategy, as we have
seen, processes are permanently assigned to a queue upon entry to the system
and processes do not move among queues. Multi-level feedback queue strategy, on
the contrary, allows a process to move between queues. The idea is to separate
out processes with different CPU burst characteristics. If a process uses too
much CPU time (that is long run process), it will be moved to a lower-priority
queue. Similarly, a process which waits too long in a lower-priority queue may
be moved to a higher-priority queue. For example, consider a multi-level
feedback queue strategy with three queues Q1, Q2 and Q3 (Fig. 7.20).
Fig. 7.20 A multi-level feedback queue.
A
process entering the system is put in queue Q1. A process in Q1 is given a time quantum t of 10 ms, say. If it does
not finish within this time, it is moved to the tail of queue Q2. If Q1 is empty, the process at
the front of queue Q2
is given a time quantum t of 20 ms, say. If it does
not complete within this time quantum, it is pre-empted and put into queue Q3. Processes in queue Q3 are serviced only when
queues Q1 and Q2 are empty.
Thus,
with this strategy, CPU first executes all processes in queue Q1. Only when Q1 is empty it will execute
all processes in queue Q2. Similarly, processes in queue Q3 will only be executed if
queues Q1 and Q2 are empty. A process which
arrives in queue Q1
will pre-empt a process in queue Q2 or Q3.
It can
be observed that, this strategy gives the highest priority to any process with
a CPU burst of 10 ms or less. Processes which need more than 10 ms, but less
than or equal to 20 ms are also served quickly, that is, it gets the next
highest priority than the shorter processes. Longer processes automatically
sink to queue Q3,
from Q3, processes will be served
in first-come first-serve (FCFS) basis and in case of process waiting for a too
long time (as decided by the scheduler) may be put into the tail of queue Q1.
Round Robin Algorithm
Round robin (RR) algorithm is a well-known scheduling algorithm
and is designed especially for time sharing systems. Here, we will see how a
circular queue can be used to implement such an algorithm. Before going to
implement the RR algorithm, we should first describe the algorithm with
illustration. Suppose, there are n processes P1, P2, . . ., Pn required to be served by
the CPU. Different processes require different execution time. Suppose,
sequence of processes’ arrivals are according to their subscripts, that is, P1 comes first than P2 and in general, Pi comes after Pi–1 for 1 < i £ n.
RR
algorithm first decides a small unit of time, called a time quantum or time
slice, t. A time quantum is generally from 10 to 100
milliseconds. CPU starts services with P1. P1 gets CPU for t instant of time, afterwards
CPU switches to P2
and so on. When CPU reaches the end of time quantum of Pn it returns to P1 and the same process will
be repeated. Now, during time sharing, if a process finishes its execution
before the finishing of its time quantum, the process then simply releases the
CPU and the next process waiting will get the CPU immediately.
For an
illustration, consider Table 5.1 for the set of processes:
Table 5.1 Table for Process and Burst Time
Process Burst
time
P1 7
P2 18
P3 5
The
total CPU time required is 30 unit. Let us assume a time quantum of 4 unit. The
RR scheduling for this will be as shown in Figure 7.21.
Fig. 7.21 RR scheduling.
The
advantages of this kind of scheduling is reducing the average turn around time
(not necessarily true always). Turn around time of a process is the time of its
completion – time of its arrival. Thus, using FCFS strategy,
Average
turn around time = unit
Whereas, using RR algorithm,
Average
turn around time unit
See the result by repeating the calculations but
using the sequence of processes as P2, P1
and P3.
In time
sharing systems any process may arrive at any instant of time. Generally, all
the process currently under executions, are maintained in a queue. When a
process finishes its execution this process is deleted from the queue and
whenever a new process arrives it is inserted at the tail of the queue and wait
for its turn. To illustrate this, let us consider Table 7.2.
Table
7.2 Table
for Process Events
Process Arrival time Burst time
P1 0 9
P2 1 3
P3 9 5
P4 14 8
Total CPU time required is 25 units. Let the time
quantum be t = 5 unit. Figure 5.23 illustrates the snapshot at
various instants with RR scheduling.
Fig. 7.22 In and out in a queue during
RR scheduling.
Now let
us discuss the implementation of RR scheduling algorithm. Circular queue is the
best choice for it. If may be noted that it is not strictly a circular queue
because here a process when it completes its execution required to be deleted
from the queue and it is not necessarily from the front of the queue rather
from any position of the queue. Except this, it follows all the properties of
queue, that is, processes which comes first gets its turn first.
Implementation
of the RR algorithm using a circular queue is straightforward. Here, we use variable
sized circular queue; size of the queue at any instant is decided by the
number of processes in execution at that instant. Another mechanism is
necessary; whenever a process is deleted, to fill the space of deleted process,
it is required to squeeze all the processes preceding to it starting from the
front pointer (Fig. 7.23). (Detail procedure for implementation is left as an
exercise to the reader.)
Fig. 7.23 Deletion of a process from
circular queue.
Queue with
Java Collection Framework
The Queue is an ordered list of objects with its use limited to insert
elements at the end of the list and deleting elements from the start of list,
that is, it follows the FIFO or the First-In-First-Out principle.
To facilitates the queue structure and its variants, java.util package
provides the Queue interface which and extends the Collection interface (see
Figure 7.24).
The another variants of queue is called deque (double ended queue),
which allows both insertion and deletion at the both ends. In JCF, the Deque
interface is defined, which extends Queue interface and the Deque Interface is
implemented by Linked List class (see Figure 7.24).
Figure 7.24. Class hierarchy of Queue and Dequeue
Few important characteristics of Queue are:
·
The
Java Queue supports all methods of Collection interface including insertion,
deletion, etc.
·
LinkedList,
ArrayBlockingQueue and PriorityQueue are the most frequently used
implementations.
·
BlockingQueues
have thread-safe implementations.
·
The
Queues which are available in java.util package are Unbounded Queues.
·
The
Queues which are available in java.util.concurrent package are the Bounded
Queues.
·
All
Queues except the Deques supports insertion and removal at the tail and head of
the queue respectively. The Deques support element insertion and removal at
both ends.
Method |
Description |
boolean add(object) |
It is used to insert the specified
element into this queue and return true upon success. |
boolean offer(object) |
It is used to insert the specified
element into this queue. |
Object remove() |
It is used to retrieves and removes
the head of this queue. |
Object poll() |
It is used to retrieves and removes
the head of this queue, or returns null if this queue is empty. |
Object element() |
It is used to retrieves, but does
not remove, the head of this queue. |
Object peek() |
It is used to retrieves, but does not
remove, the head of this queue, or returns null if this queue is empty. |
Table 7.3: The methods declared by Queue interface
Being an
interface the queue needs a concrete class for the declaration and the most
common classes are the PriorityQueue and LinkedList in Java. The PriorityQueue
class includes six constructors as summarized in Table 2.
Constructor |
Description |
PriorityQueue(
) |
The default constructor builds an
empty queue. Its starting capacity is 11. |
PriorityQueue(int
capacity) |
This constructor builds a queue
that has the specified initial capacity. |
PriorityQueue(Comparator<?
super E> comp) |
This constructor specifies
acomparator. |
PriorityQueue(int
capacity, Comparator<? super E> comp) |
This constructor builds a queue with
the specified capacity and comparator. |
PriorityQueue(Collection<?
extends E> c) |
This constructor creates a queue
which can hold a generic collection. |
PriorityQueue(PriorityQueue<?
extends E> c) |
This constructor create queues that are
initialized with the elements of the collectionpassed in c |
PriorityQueue(SortedSet<?
extends E> c) |
This constructors create queues that are
initialized with the sorted elements of the collectionpassed in c. |
Table 7.4. Constructors in
PriorityQueue class
Note: In all cases, the capacity grows automatically
as elements are added with queue structure.
All the methods declared in the Queue interface are fully defined in
the PriorityQueue class. Further, since it is a subtype of Collections class,
it inherits all the methods of it, namely size(), isEmpty(), contains() etc.
Basic operations with Queue
Java Queue supports all operations necessary to maintain a queue
structure, which are summarized in Table 7.5.
Operation |
Throws
exception |
Special
value |
Insert |
add(e) |
offer(e) |
Remove |
remove() |
poll() |
Examine |
element() |
peek() |
Table 7.5. Basic queue operations for queue
Creating a
queue
Following examples illustratehow to create queues.
Also, some basic operations with newly created queue are illustrated.
Example 7.1a.
Following example illustratehow to create a queue
using PriorityQueue class along with some common operations. Queue of string
objects
import
java.util.*;
public
class QueueCreateExample1 {
public static void main(String[] args) {
Queue<String> queue = new PriorityQueue<>();
// Adding some elements into the queue
queue.add("one");
queue.add("two");
queue.add("three");
queue.add("four");
System.out.println(queue); // Display the
contents in queue
queue.remove("three");
System.out.println(queue);
System.out.println("Queue Size:
" + queue.size());
System.out.println("Queue Contains
element 'two' or not? : " +
queue.contains("two"));
// To empty the queue
queue.clear();
}
}
Example 7.1b.
Following example illustratehow to create a queue
using LinkedList class along with some common operations. Queue of integer numbers
import
java.util.LinkedList;
import
java.util.Queue;
public
class QueueCreateExample2 {
public static void main(String[] args) {
// Declaration of a queue using LinkedList
class
Queue<Integer> q = new
LinkedList<>();
// Adds elements {0, 1, 2, 3, 4} to queue
for(int i=0; i<5; i++)
q.add(i);
// Display contents of the queue.
System.out.println("The queue contains
:"+q);
// To remove the head of queue.
int x = q.remove();
System.out.println("The element
deleted :"+ x);
System.out.println(q);
// To view the head of queue
int head = q.peek();
System.out.println("head of
queue:"+ head);
// The size of the queue
int size = q.size();
System.out.println("Size of queue:
"+ size);
}
}
Traversing a queue
Like any collection object, queue is also a collection object and it
can be traversed in many ways. Following example illustrate how to create a queue of using
user defined objects. This example defines a class Book and add
books to queue and then print all the books object in the queue. The elements
in PriorityQueue must be of Comparable type. String and Wrapper classes are
Comparable by default. To add user-defined objects in PriorityQueue, you need
to implement Comparable interface.
Example 7.2.
·
Create a queue with objects of class Book
·
Traverse the newly created queue with for-each
loop
·
Remove an element and then traverse the queue
with Iterator
import
java.util.*;
class
Book implements Comparable<Book>{
int
bookId;
String
name;
String
author;
String
publisher;
int
quantity;
public
Book(int id, String name, String author, String pub, int qty) {
bookId
= id;
this.name = name;
this.author
= author;
publisher = pub;
quantity =qty;
}
//
Defining the compareTo() method
public
int compareTo(Book b) {
if(bookId>b.bookId){
return 1;
}else
if(bookId<b.bookId){
return -1;
}else{
return 0;
}
}
}
public
class QueueCreateExample3 {
public
static void main(String[] args) {
PriorityQueue<Book>
queue=new PriorityQueue<Book>();
//Creating Books
Book b1=new Book(111,"Joy with
Java","Debasis Samanta","Elsevier",8);
Book b2=new Book(222,"Complete
Java","Herbert Schildt","Oracle",6);
Book b3=new Book(333,"Headstart
Java","Forouzan","O’Reilly",4);
//Adding
Books to the queue
queue.add(b1);
queue.add(b2);
queue.add(b3);
System.out.println("Traversing
the queue elements:");
//Traversing
queue with for-each loop
for(Book
b:queue){
System.out.println(b.bookId+"
"+b.name+" "+b.author+" "+b.publisher
+""+b.quantity);
}
// Removing a book from the queue
queue.remove();
System.out.println("After
removing one book record:");
for(Book
b:queue){
System.out.println(b.bookId+"
"+b.name+" "+b.author+" "+b.publisher+"
"+b.quantity);
}
// Adding another book into the queue
queue.add(new
Book(555, "Classic Data Structures", "D. Samanta",
"Prentice Hall", 9));
//Traversing
the queue with iterator
//Iterator
itr = queue.iterator();
//while(itr.hasNext()){
//System.out.println(itr.next().bookId+"
"+itr.name+" "+itr.author+" "+itr.publisher+"
"+itr.quantity);
//}
}
}
Copying an array of objects
to a queue
The following
program illustrates how to convert a Java array to Queue using addAll() method
defined in the Collection class.
Example 7.3:
import
java.util.*;
public
class ArrayToQueue {
public static void main(String[] args) {
// Create an array of String objects
String city[] = {"CCU","DEL","BLR","MUM","GAU"};
// Declare a queue
Queue<String> queue = new PriorotyQueue<>();
// Copying the array to Queue
Collections.addAll(queue, city);
// Printing the array
System.out.println(“Array :” + city);
System.out.println();
// Printing the queue
System.out.println(“Queue :” + queue);
}
}
Copying aqueue to an array
The following
program illustrates how to copy a Queue object to an array using toArray method
defined in the Collection class.
Example 7.4:
import
java.util.*;
public
class QueueToArray {
public static void main(String[] args) {
// Creating a queue
Queue<String> queue = new
LinkedList<>();
queue.add("Ram");
queue.add("123");
queue.add("John");
queue.add("Jaya");
queue.add("Jim");
queue.add("!@#$%");
// Copying the queue to an array of string
objects
String city[] = queue.toArray(new
String[queue.size()]);
// Printing the array
System.out.println(Arrays.toString(city));
System.out.println();
// Printing the queue
System.out.println(“Queue :” + queue);
}
}
Inserting an element into
queue
A queue in Java can grow automatically. The following program shows
how a queue structure can be created with ArrayBlocking class (same as
PriorotyQueue class) and insert elements and queue structure grows
automatically.
Example 7.5a.
import
java.util.concurrent.*;
public
class QueueInsertOperation1 {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new
ArrayBlockingQueue<>(2);
System.out.println(queue.add(1));
System.out.println(queue.add(2)); // Maximum capacity
System.out.println(queue);
System.out.println(queue.add(3)); // Queue grows dynamically
System.out.println(queue);
// Add more … to the queue
for(inti=9; i>0; i--)
queue.add(i);
//Queue grows further dynamically
System.out.println(queue);
}
}
Note:
add() returns
true if the element is inserted into queue successfully else it return false.
Example 7.5b.
Like the add(), the Queue interface has the offer() operation, which
is also used to insert new element into the queue. If it performs insert
operation successfully, it returns “true” value. Otherwise, it returns “false”
value. The following program demonstrate this functionality.
import
java.util.concurrent.*;
public
class QueueInsertOperation2 {
public static void main(String[] args) {
BlockingQueue<String> queue = new
ArrayBlockingQueue<>(2);
System.out.println(queue.offer("One"));
System.out.println(queue.offer("Two"));
System.out.println(queue);
System.out.println(queue.offer("Nine"));
System.out.println(queue);
}
}
Note:
The offer()
method insert an element at the end only
Removing element from a
queue
Thee are two methods, nameltremove() and poll() can be used to perform
delete operation in a queue structure. The delete operations returns the head
element of the queue, if it performs successfully. Queue supports delete
operation in two forms:
Queue.remove():
It throws java.util.NoSuchElementException exception if the operation fails.
Queue.poll(): It
returns a special value if the operation fails. Here, special value may be
either “false” or “null”
Example 7.6a. :Java Queue delete
operations with remove() method
import
java.util.*;
public
class QueueRemoveOperation{
public static void main(String[] args) {
Queue<String> queue = new
LinkedList<>();
queue.offer("One");
queue.offer("Two");
System.out.println(queue);
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove()); // Throws an exception
}
}
Example 7.6b. :Java Queue delete
operations with remove() method
The poll()
operation is used to delete an element from the head of the queue. If it
performs delete operation successfully, it returns the head element of the
queue. Otherwise it returns “null” value.
import
java.util.*;
public
class QueuePollOperation
{
public static void main(String[] args)
{
Queue<String> queue = new
LinkedList<>();
queue.offer("One");
queue.offer("Two");
System.out.println(queue);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll()); // It returns “null”
}
}
Accessing an element in a
Queue
There are two methods in Queue Interface, namely element() and peek(),
which are used to access an element in a queue. Following two examples
illustrates the working of the two methods.
Example 7.7a. : Accessing with element()
method
Queue.element():
The element() operation is used to retrieve an element from the head
of the queue, without removing it. If it performs examine operation
successfully, it returns the head element of the queue. Otherwise it throws
java.util.NoSuchElementException.
import
java.util.*;
public
class QueueElementOperation {
public static void main(String[] args) {
Queue<String> queue = new
LinkedList<>();
queue.add("One");
System.out.println(queue.element());
System.out.println(queue);
queue.clear();
System.out.println(queue.element()); //
Throws an exception
}
}
Example 7.7b.Accessing with peek() method
Queue.peek():
The peek() operation is used to retrieve an element from the head of
the queue, without removing it. If it performs examine operation successfully,
it returns the head element of the queue. Otherwise it returns null value.
import
java.util.*;
public
class QueuePeekOperation {
public static void main(String[] args) {
Queue<String> queue = new
LinkedList<>();
queue.add("One");
System.out.println(queue.peek());
System.out.println(queue);
queue.clear();
System.out.println(queue.peek()); //
Returns null value
}
}
Note:
If we try to call
peek() method on empty Queue, it returns null value, but does NOT throw an
exception as shown above.
Java Queue categories
In Java, we can
find many Queue implementations. We can broadly categorize them into the
following two types
·
Bounded Queues
Bounded Queues are queues which are bounded by capacity that means we
need to provide the max size of the queue at the time of creation. For
exampleArrayBlockingQueue (see previous example).
·
Unbounded Queues
Unbounded Queues are queues which are NOT bounded by capacity that
means we should not provide the size of the queue. For exampleLinkedList (see
previous example).
All Queues which
are available in java.util package are Unbounded Queues and Queues which are
available in java.util.concurrent package are Bounded Queues.
In other ways, We
can broadly categorize them into the following two types:
·
Blocking
Queues
·
Non-Blocking
Queues
All Queues which
implement BlockingQueue interface are BlockingQueues and rest are Non-Blocking
Queues.
BlockingQueues
blocks until it finishes it’s job or time out, but Non-BlockingQueues do not.
Some Queues are
Deques and some queues are PriorityQueues.
BlockingQueue operations
In addition to Queue’s two forms of operations, BlockingQueue’s
supports two more forms as shown below.
Operation |
Throws
exception |
Special
value |
Blocks |
Times
out |
Insert |
add(e) |
offer(e) |
put(e) |
offer(e,
time, unit) |
Remove |
remove() |
poll() |
take() |
poll(time,
unit) |
Examine |
element() |
peek() |
N/A |
N/A |
Table 7.6.
Operations with BlockingQueue objects
The ArrayDeque Class
The ArrayDeque class extends AbstractCollection and implements the
Deque interface. It adds no methods of its own. ArrayDeque creates a dynamic
array and has no capacity restrictions. (The Deque interface supports
implementations that restrict capacity, but does not require such
restrictions.)
ArrayDeque is a
generic class that has this declaration:
class
ArrayDeque<E>
Here, E specifies
the type of objects stored in the collection.
ArrayDeque
defines the following constructors:
ArrayDeque( )
ArrayDeque(int
size)
ArrayDeque(Collection<?
extends E> c)
The first constructor builds an empty deque. Its starting capacity is
16. The second constructor builds a deque that has the specified initial
capacity. The third constructor creates a deque that is initialized with the
elements of the collection passed in c. In all cases, the capacity grows as
needed to handle the elements added to the deque.
Example 7.8.
The following
program demonstrates ArrayDeque by using it to create a stack:
import
java.util.*;
class
ArrayDequeDemo {
public
static void main(String args[]) {
// Create an array deque.
ArrayDeque<String>adq = new
ArrayDeque<String>();
// Use an ArrayDeque like a stack.
adq.push("A");
adq.push("B");
adq.push("D");
adq.push("E");
adq.push("F");
System.out.print("Popping
the stack: ");
while(adq.peek()
!= null)
System.out.print(adq.pop()
+ " ");
System.out.println();
}
}
--- * ---